package hypeerweb;

import database.HyPeerWebDatabase;
import database.NodeProxyDB;

import java.util.*;
import java.util.concurrent.Semaphore;

import proxyCommand.*;
import visitor.*;

/**
 * A graph of one or more nodes connected in such a way as to support
 * reliability and speedy information retrieval in a peer-to-peer environment.
 * For every node in the HyPeerWeb there is a copy of that node in the database.
 * Any changes to a node in the HyPeerWeb result in a change in the
 * corresponding node in the database. When the HyPeerWeb is started, its nodes
 * are loaded from the database. For Phase 1 many of the HyPeerWeb constraints
 * are relaxed. The HyPeerWeb is a singleton.
 * 
 * <pre>
 * 	 <b>Class Domain:</b>
 * 		 singleton:		HyPeerWeb
 *   <b>Domain:</b>
 *       nodes:			Set&lt;Node&rt;
 *       hypeerwebDB:	HyPeerWebDatabase
 *   <b>Invariants:</b>
 *   	 satifies constraints in the uml diagram
 * </pre>
 * 
 * @author Brent Mouritsen
 */

public class HyPeerWeb {

	/**
	 * The Singleton variable to ensure that there will never be more than one
	 * HyPeerWeb.
	 */
	private static HyPeerWeb hypeerweb;

	/**
	 * A link to the HyPeerWebDatabase Singleton.
	 */
	private HyPeerWebDatabase hypeerwebDB;
	/**
	 * A proxy to the next segment of the hypeerweb
	 */
	private HyPeerWebProxy nextSegment;
	private int globalSize;
	/**
	 * the Object id of this segment known by other segments
	 */
	private GlobalObjectId globalObjectId;
	//private Semaphore semaphore;

	// ---------------------------------------------------------------------------------------------
	// ---------------------------------------------------------------------------------------------
	// Constructors
	/**
	 * Initializes this HyPeerWeb's domain with the parameters passed in.
	 * 
	 * @param myGlobalObjectId the HyPeerWebs new globalObjectId
	 * 
	 * 
	 */
	public HyPeerWeb(GlobalObjectId myGlobalObjectId) {
		//semaphore = new Semaphore(1, true);
		globalObjectId = myGlobalObjectId;
		nextSegment = null;
		HyPeerWebDatabase.initHyPeerWebDatabase(null);
		hypeerwebDB = HyPeerWebDatabase.getSingleton();
		HyPeerWebDatabase.loadSegment();
	}

	public HyPeerWeb(HyPeerWebProxy proxy, GlobalObjectId myGlobalObjectId) {
		//semaphore = new Semaphore(1, true);
		globalObjectId = myGlobalObjectId;
		nextSegment = proxy;
		HyPeerWebDatabase.initHyPeerWebDatabase(null);
		hypeerwebDB = HyPeerWebDatabase.getSingleton();
		HyPeerWebDatabase.loadSegment();
	}

	// ---------------------------------------------------------------------------------------------
	// ---------------------------------------------------------------------------------------------
	// GlobalObjectId Usage
	public GlobalObjectId getGlobalObjectId() {
		return globalObjectId;
	}

	public void setGlobalObjectId(GlobalObjectId newobjid) {
		globalObjectId = newobjid;
	}

	public LocalObjectId getLocalObjectId() {
		return globalObjectId.getLocalObjectId();
	}

	// ---------------------------------------------------------------------------------------------
	// ---------------------------------------------------------------------------------------------
	// GetSingleton Variations

	/**
	 * Gets the single HyPeerWeb, if one doesn't exist then one is created.
	 * 
	 * @return An instance of HyPeerWeb
	 * 
	 * @pre <i>None</i>
	 * @post singleton' = null ⇒ singleton = new HyPeerWeb() AND<br>
	 *       result = singleton
	 */
	public static HyPeerWeb getSingleton() {
		assert hypeerweb != null;

		return hypeerweb;
	}

	public static HyPeerWeb getSingleton(GlobalObjectId myGlobalObjectId) {
		if (hypeerweb == null) {
			hypeerweb = new HyPeerWeb(myGlobalObjectId);
			/* add the nodes that are already in the database */
			hypeerweb.reload();
		}

		return hypeerweb;
	}

	public static HyPeerWeb getSingleton(HyPeerWebProxy proxy,
			GlobalObjectId myGlobalObjectId) {
		if (hypeerweb == null) {
			hypeerweb = new HyPeerWeb(proxy, myGlobalObjectId);
			hypeerweb.reload();
		}
		return hypeerweb;
	}

	// ---------------------------------------------------------------------------------------------
	// ---------------------------------------------------------------------------------------------
	// Loading/Saving/Clearing the database

	/**
	 * Returns the single HyPeerWeb Database.
	 * 
	 * @return a link to the HyPeerWebDatabase Singleton
	 * 
	 * @pre <i>None</i>
	 * @post result = HyPeerWebDatabase.singleton
	 */
	public HyPeerWebDatabase getHyPeerWebDatabase() {
		return hypeerwebDB;
	}

	/**
	 * Reloads the HyPeerWeb from the database using the default database name.
	 * 
	 * @pre <i>None</i>
	 * @post all nodes are removed from the HyPeerWeb and the nodes in the
	 *       existing database are loaded into the HyPeerWeb
	 */
	public void reload() {
		//this.reload(null);
	}

	/**
	 * Reloads the HyPeerWeb from the database using the default dbName. If the
	 * dbName is null then the results are loaded from the default database.
	 * 
	 * @param string
	 *            the name of the database we want to access
	 * @pre <i>None</i>
	 * @post all nodes are removed from the HyPeerWeb and the nodes in the
	 *       database with the name 'dbName' are loaded into the HyPeerWeb. If
	 *       the 'dbName' is null nodes are loaded from the default database.
	 */
	public void reload(final String string) {
		// nodes.clear();
		// HyPeerWebDatabase.initHyPeerWebDatabase(string);
		// hypeerwebDB = HyPeerWebDatabase.getSingleton();
		// try{
		// SimplifiedNodeDomain node = hypeerwebDB.getNode(0);
		// this.addNode(node);
		// System.out.println("Adding a node");
		// }
		// catch (Exception e) {
		// System.out.println("Empty Database!");
		// }
	}

	/**
	 * Saves all the nodes in the HyPeerWeb and their corresponding information
	 * in the database.
	 * 
	 * @pre <i>None</i>
	 * @post The HyPeerWeb database only contains the nodes and corresponding
	 *       information for the current HyPeerWeb.
	 */
	public void saveToDatabase() {
		HashSet<Node> nodes = getNodes();
		nodes.addAll(NodeProxyDB.getSingleton().getAllNodeProxies());
		System.out.println("Size: " + nodes.size());
		hypeerwebDB.saveToDatabase(nodes);
	}

	public void saveHyPeerWeb() {
		if(nextSegment != null) {
			nextSegment.saveSegment(globalObjectId);
		}
		else {
			saveSegment(globalObjectId);
		}
	}
	
	public HashSet<Node> getNodes() {
		HashSet<Node> toReturn = new HashSet<Node>();

		for (Enumeration<Object> e = ObjectDB.getSingleton().enumeration(); e.hasMoreElements();) {
			Object o = e.nextElement();
			if (o instanceof Node) {
				toReturn.add((Node)o);
			}
		}
		return toReturn;
	}
	
	public void saveSegment(GlobalObjectId globalObjectId) {
		//TODO save my own database
		saveToDatabase();
		
		if(!this.getGlobalObjectId().equals(globalObjectId)) {
			nextSegment.saveSegment(globalObjectId);
		}
		GUIProxy.closeGuiAll();
		System.exit(0);
	}
	
	public void deleteSegment() {
		// TODO Delete a single segment and migrate the nodes to another segment
		if(nextSegment != null) {
			System.out.println("Migrating nodes to an existing segment!");
			
			HashSet<Node> migratingNodes = new HashSet<Node>();
			Enumeration<Object> enumerater = ObjectDB.getSingleton().enumeration();
			while (enumerater.hasMoreElements()) {
				Object obj = enumerater.nextElement();
				if (obj instanceof Node && !(obj instanceof NodeProxy)) {
					Node n = (Node) obj;
					n.migrating(true);
					migratingNodes.add(n);
				}
			}
			
			System.out.println("just about to accept Nodes!");
			nextSegment.acceptNodes(migratingNodes, globalObjectId);
			System.out.println("just about to remove segment!");
			nextSegment.removeSegment(globalObjectId, nextSegment.getGlobalObjectId());
			
			System.out.println("This segment has been deleted!");
			//nextSegment.printHyPeerWeb(globalObjectId);
			clear();
			GUIProxy.closeGuiAll();
			System.exit(0);
		}
		else {
			System.out.println("Deleting last segment of the hypeerweb!");
			clear();
			GUIProxy.closeGuiAll();
			System.exit(0);
		}
	}
	
	public boolean acceptNodes(HashSet<Node> migratingNodes, GlobalObjectId globalObjectId) {
		
		//System.out.println("Accepting Nodes");
		for (Node n : migratingNodes) {
			System.out.println("This is one of the nodes that migrated:\n" + n);
			n.updateLocalObjectId();
		}
		
		for (Node n : migratingNodes) {
			n.updateConnections(this.globalObjectId);
		}
		//ObjectDB.getSingleton().dump();
		GUIProxy.updateNodeListAll();
		return true;
	}
	
	public void removeSegment(GlobalObjectId removedSegment, GlobalObjectId removedSegmentsNextSegment) {
		if(nextSegment.getGlobalObjectId().equals(removedSegment)) {
			//PeerCommunicator.stopConnection(removedSegment);
			if(this.getGlobalObjectId().equals(removedSegmentsNextSegment)) {
				nextSegment = null;
			}
			else {
				nextSegment = new HyPeerWebProxy(removedSegmentsNextSegment);
			}
		}
		else {
			nextSegment.removeSegment(removedSegment, removedSegmentsNextSegment);
		}
	}

	/**
	 * Clears the HyPeerWeb.
	 * 
	 * @pre <i>None</i>
	 * @post |nodes| = 0
	 */
	public void clear() {
		ObjectDB.getSingleton().clear();
		ObjectDB.getSingleton().store(globalObjectId.getLocalObjectId(), this);
		hypeerwebDB.clear();
	}

	// ---------------------------------------------------------------------------------------------
	// ---------------------------------------------------------------------------------------------
	// Adding a new node

	/**
	 * Adds a node to the HyPeerWeb starting at the given startNode.
	 * 
	 * @param node
	 *            the node to be added
	 * @param startNode
	 *            the node we must start at to find the insertion node
	 * 
	 * @pre startNode is in the HyPeerWeb or null
	 * @post node will be the largest node in the HyPeerWeb
	 */
	public void addToHyPeerWeb(Node node, Node startNode) {
		if (startNode != null) {
			// System.out.println("start node does not equal null!!!!!");
			node = new Node(0);
			startNode.addToHyPeerWeb(node);
		}
		else {
			if (size() > 0) {
				// System.out.println("The size is greater than 0!!!!!");
				node = new Node(0);
				// System.out.println(nextSegment.getGlobalObjectId());
				nextSegment.insertNode(node);
			}
			else {
				new Node(0);
			}
		}
		// ObjectDB.getSingleton().dump();
	}

	public boolean insertNode(Node node) {
		System.out.println("The insert method is called!!!!");
		if (this.getNumberOfNodesOnThisSegment() == 0) {
			nextSegment.insertNode(node);
		}
		else {
			Enumeration<Object> enumerater = ObjectDB.getSingleton()
					.enumeration();
			while (enumerater.hasMoreElements()) {
				Object obj = enumerater.nextElement();
				if (obj instanceof Node && !(obj instanceof NodeProxy)) {
					((Node) obj).addToHyPeerWeb(node);
					return true;
				}
			}
		}
		return true;
	}

	public boolean addToHyPeerWeb(int p0, int p1) {
//		try {
//			semaphore.acquire();
//		}
//		catch (InterruptedException e) {
//			e.printStackTrace();
//		}
		
		Node start = getNode(p1);
		if (start == null && this.getNumberOfNodesOnThisSegment() != 0) {
			return false;
		}
		addToHyPeerWeb(null, start);

		System.out.println("Just finished adding a new node to the hypeerweb.");
		ObjectDB.getSingleton().dump();
		System.out.println(NodeProxyDB.getSingleton());
		if(nextSegment != null) {
			nextSegment.printHyPeerWeb(globalObjectId);
		}
		GUIProxy.updateNodeListAll();
		System.out.println("Added to hypeerweb");
		
//		semaphore.release();
		return true;
	}

	// ---------------------------------------------------------------------------------------------
	// ---------------------------------------------------------------------------------------------
	// Removing a node
	/**
	 * Removes the given node from the hypeerweb.
	 * 
	 * @param removeNode
	 *            the node to be removed
	 */
	public void removeFromHyPeerWeb(Node removeNode) {
		int size = size();
		if(size == 1) {
			ObjectDB.getSingleton().remove(removeNode.getLocalObjectId());
			return;
		}
		if(size == 2) {
			Node otherNode = removeNode.getFold();
			otherNode.setFold(otherNode);
			otherNode.removeNeighbor(removeNode);
			otherNode.setWebId(new WebId(0));
			
			ObjectDB.getSingleton().remove(removeNode.getLocalObjectId());
			return;
		}
		
		removeNode.removeFromHyPeerWeb(removeNode);
	}

	public boolean removeFromHyPeerWeb(int p0) {
//		try {
//			semaphore.acquire();
//		}
//		catch (InterruptedException e) {
//			e.printStackTrace();
//		}
		
		Node remove = getNode(p0);
		if (remove == null) {
			return false;
		}
		removeFromHyPeerWeb(remove);
		
		System.out.println("Just finished removing an old node to the hypeerweb.");
		ObjectDB.getSingleton().dump();
		System.out.println(NodeProxyDB.getSingleton());
		if(nextSegment != null) {
			nextSegment.printHyPeerWeb(globalObjectId);
		}
		GUIProxy.updateNodeListAll();
		System.out.println("removed from hypeerweb");
		
//		semaphore.release();
		return true;
	}

	// ---------------------------------------------------------------------------------------------
	// ---------------------------------------------------------------------------------------------
	// Get a Node

	/**
	 * Gets the node with the indicated index i.
	 * 
	 * @param id
	 *            the id of the Node we want to return
	 * @return the node with the requested id
	 * 
	 * @pre id ≥ -1
	 * @post id < size() ⇒ result = nodes[id] AND<br>
	 *       id ≥ size() ⇒ result = null;
	 */
	public Node getNode(int id) {
		Node n = null;
		for (Enumeration<Object> e = ObjectDB.getSingleton().enumeration(); e
				.hasMoreElements();) {
			Object o = e.nextElement();
			if ((o instanceof Node) && ((Node) o).webId.getValue() == id) {
				n = (Node) o;
				break;
			}
		}
		return n;
	}

	/**
	 * Determines whether the indicated node is in the HyPeerWeb.
	 * 
	 * @param node
	 *            the Node we are asking about
	 * @return whether or not the given node is in the HyPeerWeb
	 * 
	 * @pre node ≠ null
	 * @post result = node ∈ nodes
	 */
	public boolean contains(Node node) {
		return getNode(node.getWebId().getValue()) != null;
	}

	public Iterator<Node> iterator() {
		BroadcastIteratorVisitor broadcast = new BroadcastIteratorVisitor();
		Parameters p = new Parameters();
		broadcast.visit(getNode(0), p);

		return broadcast.nodes.iterator();
	}

	public ArrayList<Integer> getNodesInSegment() {
		ArrayList<Integer> ints = new ArrayList<Integer>();
		Enumeration<Object> enumerater = ObjectDB.getSingleton().enumeration();
		while (enumerater.hasMoreElements()) {
			Object obj = enumerater.nextElement();
			if (obj instanceof Node && !(obj instanceof NodeProxy)) {
				ints.add(((Node) obj).getWebId().getValue());
			}
		}
		return ints;
	}

	// ---------------------------------------------------------------------------------------------
	// ---------------------------------------------------------------------------------------------
	// Global and Local size for adding nodes and starting the GUI

	public int size() {
		if (nextSegment == null) {
			return getNumberOfNodesOnThisSegment();
		}

		nextSegment.getGlobalSize(globalObjectId, getNumberOfNodesOnThisSegment());

		return globalSize;
	}

	public boolean getGlobalSize(GlobalObjectId gbi, int count) {
		if (globalObjectId.equals(gbi)) {
			globalSize = count;
		}
		else {
			nextSegment.getGlobalSize(gbi, count + getNumberOfNodesOnThisSegment());
		}
		return true;
	}

	public int getNumberOfNodesOnThisSegment() {
		int nodeCount = 0;
		Enumeration<Object> enumerater = ObjectDB.getSingleton().enumeration();
		while (enumerater.hasMoreElements()) {
			Object obj = enumerater.nextElement();
			if (obj instanceof Node && !(obj instanceof NodeProxy)) {
				nodeCount++;
			}
		}
		return nodeCount;
	}

	// ---------------------------------------------------------------------------------------------
	// ---------------------------------------------------------------------------------------------
	// Proxy methods

	public boolean send(int p0, int p1, String p2) {
//		try {
//			semaphore.acquire();
//		}
//		catch (InterruptedException e) {
//			e.printStackTrace();
//		}
		
		Node start = getNode(p0);
		// Node end = getNode(p1);
		if (start == null) {
			return false;
		}

		GUISender s = new GUISender();
		Parameters p = GUISender.createInitialParameters(p1, p2);
		start.accept(s, p);
//		semaphore.release();
		return true;
	}

	public boolean broadcast(int p0, String p1) {
//		try {
//			semaphore.acquire();
//		}
//		catch (InterruptedException e) {
//			e.printStackTrace();
//		}
		
		Node start = getNode(p0);
		if (start == null) {
			return false;
		}

		Broadcaster b = new Broadcaster();
		Parameters p = Broadcaster.createInitialParameters(p1);
		start.accept(b, p);
//		semaphore.release();
		return true;
	}

	public GlobalObjectId connectToGUI(GlobalObjectId globalObjectId) {
//		try {
//			semaphore.acquire();
//		}
//		catch (InterruptedException e) {
//			e.printStackTrace();
//		}
		
		System.out.println("Connecting gui to hypeerweb: " + globalObjectId);
		GUIProxy.addGlobalObjectId(globalObjectId);
		
//		semaphore.release();
		return globalObjectId;
	}

	public void disconnectFromGUI(GlobalObjectId globalObjectId) {
//		try {
//			semaphore.acquire();
//		}
//		catch (InterruptedException e) {
//			e.printStackTrace();
//		}
		System.out.println("Disconnecting gui from hypeerweb: "
				+ globalObjectId);
		GUIProxy.removeGlobalObjectId(globalObjectId);
//		semaphore.release();
	}

	public void addSegment(GlobalObjectId newGlobalObjectId,
			GlobalObjectId placeInLine) {
		if (nextSegment == null) {
			nextSegment = HyPeerWebProxy.getSingleton(newGlobalObjectId);
		}
		else if (nextSegment.getGlobalObjectId().equals(placeInLine)) {
			nextSegment = HyPeerWebProxy.getSingleton(newGlobalObjectId);
		}
		else {
			nextSegment.addSegment(newGlobalObjectId, placeInLine);
		}

		System.out.println(nextSegment.getGlobalObjectId().getMachineAddr());
		System.out.println(nextSegment.getGlobalObjectId().getPortNumber());
		System.out.println(nextSegment.getGlobalObjectId().getLocalObjectId());
	}

	public void printHyPeerWeb(GlobalObjectId id) {
		if(!id.equals(globalObjectId)) {	
			GUIProxy.updateNodeListAll();
			
			System.out.println("\n//----------------------------------------------------------------------\nJust added or removed.");
			ObjectDB.getSingleton().dump();
			System.out.println(NodeProxyDB.getSingleton());
			nextSegment.printHyPeerWeb(id);
		}
	}
}